Skip to content

Conversation

@haimaychao
Copy link
Contributor

@haimaychao haimaychao commented Oct 2, 2025

Implement hybrid key exchange support for TLS 1.3 by adding three post-quantum hybrid named groups: X25519MLKEM768, SecP256r1MLKEM768, and SecP384r1MLKEM1024.
Please see JEP 527 for details about this change.


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change requires CSR request JDK-8369848 to be approved

Issues

  • JDK-8314323: Implement JEP 527: TLS 1.3 Hybrid Key Exchange (Enhancement - P2)
  • JDK-8369848: Implement JEP 527: TLS 1.3 Hybrid Key Exchange (CSR)

Contributors

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/27614/head:pull/27614
$ git checkout pull/27614

Update a local copy of the PR:
$ git checkout pull/27614
$ git pull https://git.openjdk.org/jdk.git pull/27614/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 27614

View PR using the GUI difftool:
$ git pr show -t 27614

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/27614.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented Oct 2, 2025

👋 Welcome back hchao! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Oct 2, 2025

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk
Copy link

openjdk bot commented Oct 2, 2025

@haimaychao The following label will be automatically applied to this pull request:

  • security

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@haimaychao
Copy link
Contributor Author

/contributor add @jnimeh

@haimaychao
Copy link
Contributor Author

/contributor add @wangweij

@openjdk
Copy link

openjdk bot commented Oct 2, 2025

@haimaychao
Contributor Jamil Nimeh <[email protected]> successfully added.

@openjdk
Copy link

openjdk bot commented Oct 2, 2025

@haimaychao
Contributor Weijun Wang <[email protected]> successfully added.

@haimaychao haimaychao marked this pull request as ready for review October 2, 2025 23:59
@openjdk openjdk bot added the rfr Pull request is ready for review label Oct 2, 2025
@mlbridge
Copy link

mlbridge bot commented Oct 3, 2025

Webrevs

algParams.init(keAlgParamSpec);
// Skip AlgorithmParameters for KEMs (not supported)
if (namedGroupSpec == NamedGroupSpec.NAMED_GROUP_KEM) {
if (defaultProviderName == null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We assume that if provider is not null then it must be DH without doing any checks to confirm that. It would be cleaner to call getProvider() instead.

Provider p = getProvider();
if (p == null) {
KeyFactory.getInstance(name);
} else {
KeyFactory.getInstance(name, p);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually unchanged, do you plan to do it in the next iteration?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. Changed to call getProvider() as suggested.


private static KEM getKEM(String name) throws NoSuchAlgorithmException {
if (name.startsWith("secp") || name.equals("X25519") ||
name.equals("X448")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need X448 here? Also, please provide a comment for this method with functionality description.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

X448 removed, and comment added.

X25519(32, 32,
"XDH", "XDH", NamedParameterSpec.X25519),

X448(56, 56,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need X448 and P521?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need, no. Want, yes. The support for traditional curves that are not part of the first round of hybrid KEMs lays the groundwork for future hybrid KEMs that might use these larger curves. It also gives us the base framework to move these algorithms as named groups to KEM implementations in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks for the explanation! I guess it makes sense if we expect those curves to be used in the future rounds of hybrid KEM.

implements KEMSpi.EncapsulatorSpi, KEMSpi.DecapsulatorSpi {
private final PublicKey pkR;
private final PrivateKey skR;
private final AlgorithmParameterSpec spec;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is not being used, it can be removed together with the constructor parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed variable spec.

}
} else if (k instanceof XECKey xkey
&& xkey.getParams() instanceof NamedParameterSpec ns) {
if (ns.getName().equalsIgnoreCase("X25519")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use NamedParameterSpec.X25519?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

ProtocolVersion.PROTOCOLS_TO_13,
PredefinedDHParameterSpecs.ffdheParams.get(8192)),

ML_KEM_512(0x0200, "MLKEM512",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are they needed for this Jep?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We added ML-KEM NamedGroups with null AlgorithmParameterSpec, and they won’t appear as negotiable named groups. They were added to support debug display and recognition of MLKEM named groups when used in the key share, so we can see them in debug and know if they are used. It'd help for interop debugging/testing.

// Finite Field Groups (XDH)
NAMED_GROUP_XDH("XDH", XDHScheme.instance),

NAMED_GROUP_KEM("PQC", KEMScheme.instance),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That Choice of Name needs probably an explaining comment if it is for pure PQC and/ormhybrid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can rename "PQC" to "KEM" to be consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename done.

} else { // default groups
NamedGroup[] groups = new NamedGroup[] {

// Hybrid key agreements
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like all the infra for X448MLKEM1024 is there, so rather than removing x448 from this patch, why not implement it (it’s more obvious than P511 Variants)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed the infrastructure is there, but I don't see an IETF draft that covers that hybrid variant for TLS, nor do I see an IANA mapping for it here: https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8

There needs to be a standard for TLS 1.3 backing these hybrid KEMs before we implement them.

FFDHE_2048,
FFDHE_3072,
FFDHE_4096,
FFDHE_6144,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the choise to knock out ffdhe6144 and 8192 from the default list was done on purpose. I don't think they get much use and they can always be re-enabled via SSLParameters or the system property. We're open to feedback on this if you or others feel like they should remain in place, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change is I think ok, doesn’t make much of a difference for most cases I was just thinking it needed its own commit and ticket reference but if it was intentional fine as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you make a fair point here. It probably deserves its own change, JBS entry, CSR, etc. We'll leave them in for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ffdhe6144 and 8192 added.

}

private static AlgorithmParameterSpec getSpec(String name) {
if (name.startsWith("secp")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be repeated multiple times including the case sensitive string, maybe have an APS.isGenericEC() helper?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


private static int leftPublicLength(String name) {
return switch (name.toLowerCase(Locale.ROOT)) {
case "secp256r1" -> 65;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn’t that Part formte named groups, maybe Look it up instead?


private static byte[] concat(byte[]... inputs) {
ByteArrayOutputStream o = new ByteArrayOutputStream();
Arrays.stream(inputs).forEach(o::writeBytes);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want a non presized buffer and a stream in the handshake hot path?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use presized buffer.

* @run main/othervm -Djdk.tls.namedGroups="secp384r1"
DisabledCurve DISABLE_NONE PASS
* @run main/othervm -Djdk.tls.namedGroups="secp384r1"
DisabledCurve secp384r1 FAIL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case fails whether we specify -Djdk.tls.namedGroups="secp384r1" or not, because the test certificate also uses secp384r1 algorithm in the signature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an exsiting test entry, and perahps filing a separate ticket if any change shall be made.

@mpdonova
Copy link
Contributor

mpdonova commented Oct 9, 2025

Can you add these new groups to test/jdk/javax/net/ssl/TLSCommon/NamedGroup.java?

@haimaychao
Copy link
Contributor Author

New groups added to test/jdk/javax/net/ssl/TLSCommon/NamedGroup.java.

@haimaychao haimaychao changed the title 8314323: TLS 1.3 Hybrid Key Exchange 8314323: Implement JEP 527: TLS 1.3 Hybrid Key Exchange Oct 13, 2025
@openjdk openjdk bot added the csr Pull request needs approved CSR before integration label Oct 14, 2025
@haimaychao
Copy link
Contributor Author

/csr

@openjdk
Copy link

openjdk bot commented Oct 14, 2025

@haimaychao an approved CSR request is already required for this pull request.

FFDHE6144("ffdhe6144"),
FFDHE8192("ffdhe8192");

X25519MLKEM768("X25519MLKEM768);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid syntax: The matching quotation mark is missing,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

FFDHE8192("ffdhe8192");

X25519MLKEM768("X25519MLKEM768);
SecP256r1MLKEM768("SecP256r1MLKEM768");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid syntax: enums should be comma-separated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

NamedParameterSpec.X25519MLKEM768,
"DH"),

SecP256r1MLKEM768(0x11eb, "SecP256r1MLKEM768",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the rest of named groups in this file are all upper-cased.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed hybrid groups to upper-case.

// derives the secret after receiving the client's share (KA).
// However, this is changed for KEM: the server (as encapsulator)
// must perform both actions — derive the secret and generate the
// encapsulated message at the same time during SHKeyShareProducer.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: during SHKeyShareProducer doesn't seem gramatically correct, did you mean during encapsulation in SHKeyShareProducer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, comment updated.

// Traditional Key Agreement (KA):
// Both peers perform similar operations: generate a public key,
// send it, and compute a shared secret upon receiving the peer's
// public key.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We use both public key and key share in this comment when describing Traditional Key Agreement. I think we should use only key share (as in RFC 8446) when describing a traditional key exchange to avoid any confusion with KEM's public key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the comment.

*
* @since 26
*/
public static final NamedParameterSpec SecP384r1MLKEM1024
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Let's make these all upper-case as well to keep things consistent across files.


X25519MLKEM768("X25519MLKEM768"),
SecP256r1MLKEM768("SecP256r1MLKEM768"),
SecP384r1MLKEM1024("SecP384r1MLKEM1024");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Make those uppercase.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We keep the actual name strings the same as in spec, the enum variables are to be uppercase for consistency.

}
}

throw new InvalidKeySpecException(keySpec.toString());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check null keySpec, and throw new InvalidKeySpecException("keySpec must not be null").

For non-null keySpec, I may use:
throw new InvalidKeySpecException(keySpec.getClass().getName() + " not supported.");

public record SecretKeyImpl(SecretKey k1, SecretKey k2) implements SecretKey {
@Override
public String getAlgorithm() {
return "Hybrid";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this name be added to CSR and Java Security Standard Algorithm Names doc?

"ED25519", "ED448", "X25519", "X448",
"ML_DSA_44", "ML_DSA_65", "ML_DSA_87",
"ML_KEM_512", "ML_KEM_768", "ML_KEM_1024",
"X25519MLKEM768", "SecP256r1MLKEM768", "SecP384r1MLKEM1024",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it sound like an option to use the similar naming style like "ML_KEM_512" for the new names? for example, X25519MLKEM768 -> X25519_ML_KEM_768

*/
private static KEM getKEM(String name) throws NoSuchAlgorithmException {
if (APS.isGenericEC(name) || APS.isXDH(name)) {
return KEM.getInstance("DH", DH.PROVIDER);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I get more information about the hard-coded provider DH.PROVIDER? Could it be more general so that other KEM provider can also be used?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the DH provider is there give a KEM-like face on the NIST curves, x25519/448, etc., mainly for the purposes of assisting the work done within the hybrid KEM. The underlying work being done by the internal DH provider still ends up doing KeyAgreement, KeyPairGenerator and KeyFactory operations, and those should go through the usual provider selection process.

// DH in its own private provider so we always getInstance from here.
public static final Provider PROVIDER = new ProviderImpl();

private static class ProviderImpl extends Provider {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not expose the Provider to public?

protected PublicKey engineGeneratePublic(KeySpec keySpec)
throws InvalidKeySpecException {
if (keySpec instanceof RawKeySpec rks) {
byte[] key = rks.getKeyArr();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null check on key may be missed. If the API expose to public in the future, the key length checking may be good before Arrays.copyOfRange.

Comment on lines +195 to +197
} catch (Exception e) {
leftKey = left.generatePublic(new X509EncodedKeySpec(
leftKeyBytes));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fall-back is not good for performance. Does it have to support two key spec?

left.initialize(leftSpec, random);
right.initialize(rightSpec, random);
initialized = true;
} catch (Exception e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be nice if not wrap InvalidParameterException twice.

Comment on lines +141 to +149
if (!initialized) {
try {
left.initialize(leftSpec);
right.initialize(rightSpec);
initialized = true;
} catch (Exception e) {
throw new ProviderException(e);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The initialization block could be saved if call initialize in the constructor. See sun/security/ec/ECKeyPairGenerator.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

csr Pull request needs approved CSR before integration rfr Pull request is ready for review security [email protected]

Development

Successfully merging this pull request may close these issues.

7 participants